Skip to content

shortenFullyQualifiedTypes: preserve unqualified type resolution - #3037

Open
maxandersen wants to merge 2 commits into
diffplug:mainfrom
maxandersen:3033-unqualified-type-collision
Open

shortenFullyQualifiedTypes: preserve unqualified type resolution#3037
maxandersen wants to merge 2 commits into
diffplug:mainfrom
maxandersen:3033-unqualified-type-collision

Conversation

@maxandersen

@maxandersen maxandersen commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR combines two fixes for shortenFullyQualifiedTypes:

Fix 1: Preserve unqualified type resolution (#3033)

When a file already uses a type name unqualified (e.g. extends RandomAccessFile), the formatter must not add an import for a different fully-qualified type with the same simple name — that would silently change what the existing reference resolves to.

What changed: collect all unqualified ClassOrInterfaceType references and skip shortening any FQN whose simple name matches, unless the type is already explicitly or implicitly imported.

Fix 2: Shorten FQTs in expression context (#3039)

Previously, shortenFullyQualifiedTypes only handled type-context references (ClassOrInterfaceType AST nodes — declarations, generics, casts, etc.). FQTs used in expression context were silently ignored:

  • Static method calls: java.lang.management.ManagementFactory.getPlatformMXBeans(…)
  • Static field / enum constant access: java.util.concurrent.TimeUnit.SECONDS
  • Nested-type member access: pkg.models.CustomTypeProperty.TypeEnum.STRING

What changed: the AST walker now also visits MethodCallExpr and FieldAccessExpr nodes, walking their scope chains to find package-qualified type references.

Safety: never introduce a compile error

Since we run without a classpath, expression-context FQTs (which JavaParser sees as expressions, not types) need extra care to avoid false positives like shortening variable.Field into a bogus import. Two heuristics guard against this:

  1. Known-package check — if the candidate FQN's package already appears in the file's imports, own package declaration, or is java.lang, we trust it.
  2. Minimum-depth fallback — otherwise, require ≥ 2 lowercase (package) segments before the first uppercase (type) segment. This filters out config.Default.VALUE or builder.Type.create() patterns where the first segment is likely a local variable, not a package. In theory this skips a legitimate single-segment package (a.MyType) with no matching import, but single-segment packages are virtually non-existent in practice.

When we know something is ambiguous (simple name clashes with an existing import, a declared type, or an unqualified reference), we always leave it qualified.

Tests

31 tests total (9 new for expression-context, 1 new for unqualified collision):

Category Examples
Should shorten (expression) static method call, static field, enum constant, chained call, java.lang.System.exit()
Should NOT shorten (ambiguous) single-segment unknown package, import collision, declared-type collision
Should NOT shorten (unqualified collision) same-package type used unqualified + different FQT with same simple name
./gradlew :testlib:test --tests com.diffplug.spotless.java.ShortenFullyQualifiedTypesStepTest

…lug#3039)

Extend the AST walker to also visit MethodCallExpr and FieldAccessExpr
nodes, covering static method calls (ManagementFactory.getPlatformMXBeans),
static field/enum access (TimeUnit.SECONDS), and nested-type member access
(CustomTypeProperty.TypeEnum.STRING).

Expression context lacks type-node certainty, so two heuristics guard
against false positives:
1. Known-package check — trust if the candidate package appears in
   existing imports, the file's own package, or java.lang.
2. Minimum-depth fallback — otherwise require >= 2 lowercase segments
   before the first uppercase segment, filtering variable.Field patterns.

Document the safety contract (never introduce a compile error) in both
source javadoc and test class javadoc.

Add 9 new tests covering expression-context shortening and ambiguous
cases that must be left alone.
@maxandersen

Copy link
Copy Markdown
Contributor Author

given #3033 and #3039 i've updated this pr to fix those issues that require parsing deeper to catch more unnecesary fully qualified names. Given we dont have access to full compiler context there are cases where we just can't know and here we fallback to heuristics, example:

a.Value could both be a class named Value inside a package or class naemd a' or a field named Valueinside a class nameda'. Here we dont consider it a fully qualfied name unless there are two dots (i.e. a.b.Valuewill be shortened a.Value would not).

And even in that case we try and identify if there are existing imports to guide/ensure we don't break code.

Full details in the javadoc and body of description.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant